/******************************************************************************
	Cobalt - utility libraries for building XNA games
	Copyright (C) 2008 Philip Djonov

	This program is free software; you can redistribute it and/or modify it
	under the terms of the GNU General Public License as published by the Free
	Software Foundation; either version 2 of the License, or (at your option)
	any later version.

	This program is distributed in the hope that it will be useful, but
	WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
	or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
	for more details.

	You should have received a copy of the GNU General Public License along
	with this program; if not, write to the
	
		Free Software Foundation, Inc.
		51 Franklin Street, Fifth Floor
		Boston, MA  02110-1301, USA.
******************************************************************************/

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Graphics.PackedVector;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;

namespace Cobalt.Content.Pipeline.Quake.Bsp
{
	using Graphics;

	[ContentProcessor( DisplayName = "BSP Processor" )]
	public class BspContentProcessor : ContentProcessor<ImportedBspContent, CompiledBsp>
	{
		#region Processor Properties
		private bool flipCoordinates = true;
		[DefaultValue( true )]
		public bool FlipCoordinates
		{
			get { return flipCoordinates; }
			set { flipCoordinates = value; }
		}

		private bool generateTangents = false;
		[DefaultValue( false )]
		public bool GenerateTangents
		{
			get { return generateTangents; }
			set { generateTangents = value; }
		}

		private int maxLightmapSize = 512;
		[DefaultValue( 512 )]
		public int MaxLightmapSize
		{
			get { return maxLightmapSize; }
			set { maxLightmapSize = value; }
		}

		private bool allowNPO2Lightmaps = false;
		[DefaultValue( false )]
		public bool AllowNPO2Lightmaps
		{
			get { return allowNPO2Lightmaps; }
			set { allowNPO2Lightmaps = value; }
		}

		private int pvsClusterSizeTarget = 2000;
		[DefaultValue( 2000 )]
		public int PvsClusterSizeTarget
		{
			get { return pvsClusterSizeTarget; }
			set { pvsClusterSizeTarget = value; }
		}

		[DefaultValue( false )]
		public bool GenerateParallaxTechniques { get; set; }
		#endregion

		private ImportedBspContent input;
		private CompiledBsp output;
		private ContentProcessorContext context;
		private Dictionary<GeometryContent, Surface> geometryMap;

		private BspProcessor processor = new BspProcessor();

		public override CompiledBsp Process( ImportedBspContent input, ContentProcessorContext context )
		{
			this.input = input;
			this.output = new CompiledBsp();
			this.geometryMap = new Dictionary<GeometryContent, Surface>();
			this.context = context;

			processor.Logger = context.Logger;

			if( GenerateParallaxTechniques && !GenerateTangents )
				context.Logger.LogWarning( string.Empty, input.Identity, "GenerateTangents is false, but a tangent basis will be generated anyway because GeneratePomShaders is true." );

			//this reduces the workload further down the pipe
			WeldGeometryPositions();

			if( FlipCoordinates )
				processor.FlipCoordinates( input );

			output.Entities.AddRange( processor.CompileEntities( input ) );

			CompileLightmaps();
			CompileBrushes();		
			CompilePvs();
			CompileGeometry();
			CompileNodes();
			CompileMaterials();
			UpdateOutputIndices();

			CompiledBsp ret = this.output;

			this.context = null;
			this.geometryMap = null;
			this.output = null;
			this.input = null;

			return ret;
		}

		private void UpdateOutputIndices<T>( List<T> list ) where T : BspItem
		{
			for( int i = 0; i < list.Count; i++ )
				list[i].Index = i;
		}

		private void UpdateOutputIndices()
		{
			UpdateOutputIndices( output.Brushes );
			UpdateOutputIndices( output.Entities );
			UpdateOutputIndices( output.Buffers );
			UpdateOutputIndices( output.PartitionNodes );
			UpdateOutputIndices( output.LeafNodes );
			UpdateOutputIndices( output.Surfaces );
		}

		private void CompileLightmaps()
		{
			processor.CombineLightmaps( input, maxLightmapSize,
				allowNPO2Lightmaps || context.TargetPlatform == TargetPlatform.Xbox360 );
			
			processor.ReformatLightmaps( input );

			processor.CollapseDuplicateMaterials( input );

			output.Lightmaps.AddRange( input.Lightmaps );
		}

		#region Geometry processing functions

		private void WeldGeometryPositions()
		{
			int prevPosCount = input.Mesh.Positions.Count;
			MeshHelper.MergeDuplicatePositions( input.Mesh, 0 );
			if( input.Mesh.Positions.Count != prevPosCount )
				context.Logger.LogMessage( "Welded away {0} duplicate vertex positions.", prevPosCount - input.Mesh.Positions.Count );
		}

		private void CompileGeometry()
		{
			processor.RemoveEmptySurfaces( input );
			processor.MergeSurfaces( input, processor.GetMergeGroups( input ) );

			bool genTanBasis = GenerateTangents || GenerateParallaxTechniques;

			if( genTanBasis )
			{
				MeshHelper.CalculateTangentFrames( input.Mesh, VertexChannelNames.TextureCoordinate( 0 ),
					VertexChannelNames.Tangent( 0 ), VertexChannelNames.Binormal( 0 ) );
			}

			MeshHelper.MergeDuplicateVertices( input.Mesh );
			MeshHelper.OptimizeForCache( input.Mesh );

			foreach( GeometryContent geom in input.Mesh.Geometry )
			{
				geom.Vertices.Channels.ConvertChannelContent<NormalizedShort4>( VertexChannelNames.Normal( 0 ) );

				if( genTanBasis )
				{
					geom.Vertices.Channels.ConvertChannelContent<NormalizedShort4>( VertexChannelNames.Tangent( 0 ) );
					geom.Vertices.Channels.ConvertChannelContent<NormalizedShort4>( VertexChannelNames.Binormal( 0 ) );
				}
			}

			processor.RescaleVertexLighting( input );

			List<GeometryContent> geometries = new List<GeometryContent>( input.Mesh.Geometry );
			geometries.Sort( ( a, b ) => input.Materials.IndexOf( (ImportedMaterialContent)a.Material ) - input.Materials.IndexOf( (ImportedMaterialContent)b.Material ) );

			for( int e, s = 0; s < geometries.Count; s = e )
			{
				int vertCount = geometries[s].Vertices.VertexCount;

				for( e = s + 1; e < geometries.Count; e++ )
				{
					int newVertCount = vertCount + geometries[e].Vertices.VertexCount;

					if( newVertCount >= ushort.MaxValue )
						break;

					vertCount = newVertCount;
				}

				CompileSurfaceGeometry( geometries, s, e );
			}
		}

		private void CompileSurfaceGeometry( List<GeometryContent> geometries, int startIndex, int endIndex )
		{
			VertexElement[] decl = null;
			List<VertexBufferContent> bufs = new List<VertexBufferContent>( endIndex - startIndex );
			List<IndexCollection> idxs = new List<IndexCollection>( endIndex - startIndex );

			GeometryBuffer geoBuf = new GeometryBuffer();
			output.Buffers.Add( geoBuf );

			int byteCount = 0;
			int baseVertex = 0;
			int indexCount = 0;

			for( int i = startIndex; i < endIndex; i++ )
			{
				GeometryContent geom = geometries[i];

				VertexBufferContent vb;
				geom.Vertices.CreateVertexBuffer( out vb, out decl, context.TargetPlatform );

				IndexCollection ib = new IndexCollection();
				foreach( int idx in geom.Indices )
					ib.Add( idx + baseVertex );

				bufs.Add( vb );
				idxs.Add( ib );

				Surface surf = new Surface( geoBuf );
				surf.FirstVertex = baseVertex;
				surf.VertexCount = geom.Vertices.VertexCount;
				surf.FirstIndex = indexCount;
				surf.IndexCount = geom.Indices.Count;

				output.Surfaces.Add( surf );

				geometryMap.Add( geom, surf );

				byteCount += vb.VertexData.Length;
				baseVertex += geom.Vertices.VertexCount;
				indexCount += geom.Indices.Count;
			}

			geoBuf.Vertices = CombineVertexBuffers( bufs, byteCount );
			geoBuf.VertexDeclaration = decl;
			geoBuf.Indices = CombineIndexBuffers( idxs );
		}

		private VertexBufferContent CombineVertexBuffers( List<VertexBufferContent> vbs, int totalSize )
		{
			VertexBufferContent ret = new VertexBufferContent( totalSize );

			int offset = 0;
			foreach( VertexBufferContent vb in vbs )
			{
				Array.Copy( vb.VertexData, 0, ret.VertexData, offset, vb.VertexData.Length );
				offset += vb.VertexData.Length;
			}

			return ret;
		}

		private IndexCollection CombineIndexBuffers( List<IndexCollection> ibs )
		{
			IndexCollection ret = new IndexCollection();

			foreach( IndexCollection ib in ibs )
				ret.AddRange( ib );

			return ret;
		}

		#endregion

		#region Brushes
		private Dictionary<ImportedCollisionBrushContent, Brush> brushMap = new Dictionary<ImportedCollisionBrushContent, Brush>();
		private void CompileBrushes()
		{
			foreach( ImportedCollisionBrushContent inBrush in input.CollisionBrushes )
			{
				Brush newBrush = new Brush();

				newBrush.ContentFlags = inBrush.Shader.ContentFlags;

				foreach( ImportedCollisionBrushSide inSide in inBrush.Sides )
				{
					BrushSide newSide = new BrushSide();

					newSide.Plane = inSide.Plane;
					newSide.SurfaceFlags = inSide.Shader.SurfaceFlags;
					
					newBrush.Sides.Add( newSide );
				}

				output.Brushes.Add( newBrush );
				brushMap.Add( inBrush, newBrush );
			}
		}
		#endregion

		#region Pvs
		private void CompilePvs()
		{
			if( pvsClusterSizeTarget < 0 )
			{
				foreach( var node in input.Leaves )
					node.Cluster = null;
				return;
			}
			else
			{
				processor.SimplifyPvs( input, pvsClusterSizeTarget );
				BuildPvsMask();
			}
		}

		private void BuildPvsMask()
		{
			List<PvsCluster> clusters = input.PvsData.Clusters;

			int clusterInts = (clusters.Count / 32) + 1;
			uint[] pvsMask = new uint[clusters.Count * clusterInts];

			for( int i = 0; i < clusters.Count; i++ )
			{
				int offset = i * clusterInts;
				foreach( PvsCluster link in clusters[i].Links )
					pvsMask[offset + (link.ClusterNum >> 5)] |= 1U << (link.ClusterNum & 0x1F);

				pvsMask[offset + (i >> 5)] |= 1U << (i & 0x1F);
			}

			output.PvsData = new BspPvs();
			output.PvsData.Clusters.AddRange( input.PvsData.Clusters );
			output.PvsData.PvsConnectivityBits = pvsMask;
		}
		#endregion

		#region Nodes
	
		private void CompileLeafNodes( Dictionary<ImportedNodeContent, BspNode> nodeMap )
		{		
			foreach( ImportedNodeContent node in input.Leaves )
			{
				BspLeafNode newLeaf = new BspLeafNode();

				newLeaf.Bounds = node.BoundingBox;
				newLeaf.PvsCluster = node.Cluster;

				for( int i = 0; i < node.Surfaces.Count; i++ )
				{
					Surface surf;
					if( geometryMap.TryGetValue( node.Surfaces[i], out surf ) )
						newLeaf.Surfaces.Add( surf );
				}

				for( int i = 0; i < node.Brushes.Count; i++ )
					newLeaf.Brushes.Add( brushMap[node.Brushes[i]] );

				nodeMap[node] = newLeaf;

				output.LeafNodes.Add( newLeaf );
			}
		}

		private void CompilePartitionNodes( Dictionary<ImportedNodeContent, BspNode> nodeMap )
		{
			List<BspPartitionNode> partitions = new List<BspPartitionNode>();

			foreach( ImportedNodeContent node in input.Nodes )
			{
				BspPartitionNode newNode = new BspPartitionNode();

				newNode.Plane = node.Plane;
				newNode.Bounds = node.BoundingBox;

				partitions.Add( newNode );

				nodeMap[node] = newNode;
			}

			int i = 0;
			foreach( ImportedNodeContent node in input.Nodes )
			{
				if( node.Child0 != null )
					partitions[i].Child0 = nodeMap[node.Child0];
				if( node.Child1 != null )
					partitions[i].Child1 = nodeMap[node.Child1];

				i++;
			}

			Queue<BspPartitionNode> insertOrder = new Queue<BspPartitionNode>();
			//find the world spawn node (*should* be node zero)
			insertOrder.Enqueue( partitions[0] );

			while( insertOrder.Count > 0 )
			{
				BspPartitionNode node = insertOrder.Dequeue();

				BspPartitionNode child;

				child = node.Child0 as BspPartitionNode;
				if( child != null )
					insertOrder.Enqueue( child );

				child = node.Child1 as BspPartitionNode;
				if( child != null )
					insertOrder.Enqueue( child );

				output.PartitionNodes.Add( node );
			}
		}

		private void CompileNodes()
		{
			var nodeMap = new Dictionary<ImportedNodeContent, BspNode>();

			CompileLeafNodes( nodeMap );
			CompilePartitionNodes( nodeMap );
		}
		#endregion

		#region Shader stuff
		private string GetQuake3FileSystemRoot( string bspPath )
		{
			string bspDir = Path.GetDirectoryName( bspPath );
			string bspDirName = Path.GetFileName( bspDir );

			if( string.Compare( bspDirName, "maps", StringComparison.InvariantCultureIgnoreCase ) != 0 )
				return null;

			return Path.GetDirectoryName( bspDir );
		}

		private string GetContentRoot( string path )
		{
			while( path != null )
			{
				string dir = Path.GetFileName( path );

				if( string.Compare( dir, "Content", StringComparison.InvariantCultureIgnoreCase ) == 0 )
					return path;

				path = Path.GetDirectoryName( path );
			}

			return null;
		}

		private string defaultMaterial = @"Materials\Default.mat";
		[System.ComponentModel.DefaultValue( @"Materials\Default.mat" )]
		public string DefaultMaterial
		{
			get { return defaultMaterial; }
			set { defaultMaterial = value; }
		}

		private string q3root, contentRoot;

		private void CompileMaterials()
		{
			q3root = GetQuake3FileSystemRoot( input.Identity.SourceFilename );
			if( q3root == null )
			{
				context.Logger.LogMessage( "Couldn't find q3 root directory." );
				return;
			}

			contentRoot = GetContentRoot( Path.GetDirectoryName( input.Identity.SourceFilename ) );
			if( contentRoot == null )
			{
				context.Logger.LogMessage( "Couldn't find content root directory." );
				return;
			}

			string defMatPath = Path.Combine( contentRoot, defaultMaterial );
			if( !File.Exists( defMatPath ) )
			{
				context.Logger.LogMessage( "Default material file not found." );
				return;
			}

			OpaqueDataDictionary matProcParams = new OpaqueDataDictionary();
			matProcParams["BasePath"] = Path.GetFileName( q3root );
			matProcParams["GenerateParallaxTechnique"] = GenerateParallaxTechniques;

			Dictionary<MaterialContent, MaterialContent> matMap = new Dictionary<MaterialContent, MaterialContent>();

			foreach( ImportedMaterialContent mat in input.Materials )
			{
				context.Logger.LogMessage( "BSP material '{0}', {1}.",
					mat.Shader.Name, output.Lightmaps.IndexOf( mat.Lightmap ) );

				CobaltMaterialContent newMat;
				try
				{
					mat.Identity = input.Identity;
					newMat = context.Convert<ImportedMaterialContent, CobaltMaterialContent>( mat, "Cobalt_Quake_MaterialProcessor", matProcParams );
				}
				catch( Exception ex )
				{
					context.Logger.LogWarning( string.Empty, mat.Identity, "Error compiling material {0}: {1}\n{2}\n{3}",
						mat.Name, ex.GetType().Name, ex.Message, ex.StackTrace );
					newMat = null;
				}

				if( newMat == null )
				{
					matMap.Add( mat, null );
					continue;
				}

				if( mat.Lightmap != null )
					newMat.PipelineTags["Lightmap"] = mat.Lightmap;

				output.Materials.Add( newMat );
				matMap.Add( mat, newMat );
			}

			foreach( var surf in geometryMap )
			{
				if( surf.Value == null || surf.Key.Material == null )
					continue;

				surf.Value.Material = matMap[surf.Key.Material];
			}
		}
		#endregion
	}
}